导航菜单
首页 >  2024腾讯游戏安全PC初赛复现  > 腾讯游戏安全2024安卓初赛复现

腾讯游戏安全2024安卓初赛复现

这个可能是属于UE4逆向首先要做的事情,网上关于这一步有很多教程,这里便不再详说。看UE4具体版本:

版本是4.27将UE4.so拖入IDA,IDA分析完后获取三个主要结构的偏移。GWorld:0x0B32D8A8GName:0x0B171CC0GUObjectArray:0xB1B5F98

拿到dump下来的SDK后应该就可以通过它来锁定实例对一些属性进行一个修改。不过这块我要怎么实现呢?写frida脚本吗?是的,写frida脚本,但是这和我之前写的一些小小的勾取脚本不同,这次要锁定类的方法是根据地址。

下文的代码块中基本都是JS代码,实现一些功能的函数。

这里有一步操作是我们可以从SDK_dump文件中获取到很多函数及类的偏移。将K2_GetActorLocation函数的偏移传入构造JavaScript函数,然后将玩家的类地址和移动到的坐标作为参数即可调用我们自定义的函数。

将获取到的坐标进行一个搜索冻结后实现一个方向无法移动,证实该坐标的正确。

从之前的SDK_dump中获取函数的偏移构建setActorLocation函数

这里只要调用该函数即可实现坐标的变化。瞬移到此结束。虽然瞬移到此结束,但是这里我要说明一下这里有关函数类封装性的分析。

这里拿SetActorLocation来做个例子。dump下的SDK中相关的该函数地址为0x965dc3c。转到该地址后实际参数只有三个。这里的话,要关联一些关于SDK的知识。SDK中看到的函数地址是中转函数。这里的三个参数数量是正确的,分别是类指针,存放参数用来解析的结构体,存放返回值的指针,它是一个中转函数。

在下文的return这里的这个函数才是实际和我们源码看到的相符的。而我们再进入这个函数时,可以看到和源码中被return的那个函数相符的部分。所以sub_8C3181C这里才是我们实际要调用的函数。

通过改变玩家高度大概锁定了天上的flag在高度3000左右往上。这里就是对所有高度大于3000的对象实例尽可能调用SetVisibility函数来使得Actor可见。第一个参数为Component型所以传入Actor对象实例的0x130偏移为指针。

然后试图对所有对象进行调用。这里发现问题似乎在于函数的使用,这里使用类本身的setStaticMeshActorCollisionEnabled()可以实现立方体可碰撞,但是我选用上面的更为全局的碰撞函数setActorEnableCollision()时却无法实现。这里不太清楚为什么,把section3搞完再去搜一下。

解读一下第二种碰撞函数:对actor加上0x220的偏移,定位到StaticMeshComponent* StaticMeshComponent,读取指针定位到StaticMeshComponent对象,再读取一次指针定位到StaticMeshComponent对象的虚函数表。然后加上0x660的偏移读取SetCollisionEnable虚函数地址。

碰撞了所有的三个立方体后天上又出现flag的一部分。第三个是直接找到了SetCollisionEnabled的地址然后构建函数。

第一个无法实现,第二个和第三个本质上是一样的,可以实现功能。

这个类的方法直接猜了下和getflag这种字符串有关,直接在sdkdump中搜到了。但是正解我还需要再学习一下。找到该函数后直接到目标地址。dlopen是一个UE4.so中专门的函数。调用了libplay中的get_last_flag函数。再次进入后发现一堆异或。

这段异或尝试后发现是base64的码表。打算通过动调理清这段加密流程。在手机端启动idaserver。然后以该命令以调试状态启动目标app

可以Attach到进程,但是F9之后没有任何反应,手机显示wait for debug,电脑直接跑飞。看来动调并不太行,于是转而去读汇编。

E4C这一段下面的BR会根据一些比较来决定跳转到下面的EA8还是ED8,这个让gpt就可以分析出来。大致分析EA8是主要加密段,进行一个异或操作,下面的ED8类似RET,进行一个结束处理。(分析不一定准确,因为不会ARM汇编,只能结合AI)

再分析下面的函数,进入后发现EEC这个数组异或完后猜测是base64变表,那这个函数大抵就是base64了。再往下的部分大概就是比较了。汇编喂给GPT后分析出大致是24个字节的比较。这里就能推断出上文其中一个进行异或的数组是密文异或后:

这段数组应该就是作为EA8段异或操作的key数组。根据这些写出脚本:

拿到最后一部分flag:_Anti_Cheat_Expert

flag:FLAG{8939008_Anti_Cheat_Expert}在复习的过程中还是学了不少东西的,也多亏了有两位师傅的无私解疑和指导,写的文章还很粗糙,需要时间打磨。最后在这里贴上frida的脚本供于参考。

以及我学习中有看的一些文章不错文章:http://www.yxfzedu.com/article/670不错文章:https://iosre.com/t/%E8%AE%B0%E5%BD%95%E4%B8%80%E6%AC%A1%E8%99%9A%E5%B9%BB4ue4%E6%89%8B%E6%B8%B8%E9%80%86%E5%90%91/19808好文章:http://www.yxfzedu.com/article/10613

./Ue4dumper --package com.tencent.ace.match2024 --ptrdec --sdku --gname 0x0B171CC0 --guobj 0x0B1B5F98 --output /data/local/tmp --newue+./Ue4dumper --package com.tencent.ace.match2024 --ptrdec --sdku --gname 0x0B171CC0 --guobj 0x0B1B5F98 --output /data/local/tmp --newue+function set(moduleName){  //获取libUE4的基地址  moduleBase=Module.findBaseAddress(moduleName);   //UE4基地址加上在IDA中获取的GName偏移获取GName地址  GName=moduleBase.add(GName_Offset);   //同上获取GUObjectArray的地址  GUObjectArray=moduleBase.add(GUObjectArray_Offset); //读取GWorld的指针  GWorld =moduleBase.add(GWorld_Offset).readPointer(); }function setPlayerHP(hp=1000000){  //生命值属性的偏移  getPlayAddr().add(0x510).writeFloat(hp);//写入}function set(moduleName){  //获取libUE4的基地址  moduleBase=Module.findBaseAddress(moduleName);   //UE4基地址加上在IDA中获取的GName偏移获取GName地址  GName=moduleBase.add(GName_Offset);   //同上获取GUObjectArray的地址  GUObjectArray=moduleBase.add(GUObjectArray_Offset); //读取GWorld的指针  GWorld =moduleBase.add(GWorld_Offset).readPointer(); }function setPlayerHP(hp=1000000){  //生命值属性的偏移  getPlayAddr().add(0x510).writeFloat(hp);//写入}getNameId: function(obj){  console.log("obj:${obj}");  try{    var nameId=ptr(obj).add(offset_UObject_FNameIndex).readU32();//读取4字节。    //console.log("nameId:${nameId}");    return nameId;  }catch(e){    console.log("error")    return 0;  }}  function getFNameFromID(index) {  // FNamePool相关偏移量和步长  var FNameStride = 0x2;                   // FNameEntry 的步长,每个FNameEntry占用2字节  var offset_GName_FNamePool = 0x30;       // GName 到 FNamePool 的偏移量  var offset_FNamePool_Blocks = 0x10;      // FNamePool 到 Blocks 的偏移量     // FNameEntry相关偏移量和位  var offset_FNameEntry_Info = 0;          // FNameEntry 到 Info 的偏移量  var FNameEntry_LenBit = 6;               // FNameEntry 长度位  var offset_FNameEntry_String = 0x2;      // FNameEntry 到字符串部分的偏移量   // 计算块和偏移量  var Block = index >> 16;                 // 块索引  var Offset = index & 65535;              // 块内偏移量   // 获取FNamePool的起始地址  var FNamePool = GName.add(offset_GName_FNamePool);  // console.log(`FNamePool: ${FNamePool}`);  // console.log(`Block: ${Block}`);     // 获取特定块的地址  var NamePoolChunk = FNamePool.add(offset_FNamePool_Blocks + Block * 8).readPointer();  // console.log(`NamePoolChunk: ${NamePoolChunk}`);     // 计算FNameEntry的地址  var FNameEntry = NamePoolChunk.add(FNameStride * Offset);  // console.log(`FNameEntry: ${FNameEntry}`);     try {      // 读取FNameEntry的Header      if (offset_FNameEntry_Info !== 0) {          var FNameEntryHeader = FNameEntry.add(offset_FNameEntry_Info).readU16();           } else {          var FNameEntryHeader = FNameEntry.readU16();      }  } catch(e) {      // 捕捉读取异常并返回空字符串      // console.log(e);      return "";  }  // console.log(`FNameEntryHeader: ${FNameEntryHeader}`);     // 获取字符串地址  var str_addr = FNameEntry.add(offset_FNameEntry_String);  // console.log(`str_addr: ${str_addr}`);     // 计算字符串长度和宽度  var str_length = FNameEntryHeader >> FNameEntry_LenBit; // 计算字符串长度  var wide = FNameEntryHeader & 1;                       // 判断字符串是否为宽字符     // 如果是宽字符,返回 "widestr"  if (wide) return "widestr";   // 如果字符串长度合理,读取并返回UTF-8字符串  if (str_length > 0 && str_length < 250) {      var str = str_addr.readUtf8String(str_length);      return str;  } else {      return "None"; // 长度不合理,返回 "None"  }}getNameId: function(obj){  console.log("obj:${obj}");  try{    var nameId=ptr(obj).add(offset_UObject_FNameIndex).readU32();//读取4字节。    //console.log("nameId:${nameId}");    return nameId;  }catch(e){    console.log("error")    return 0;  }}  function getFNameFromID(index) {  // FNamePool相关偏移量和步长  var FNameStride = 0x2;                   // FNameEntry 的步长,每个FNameEntry占用2字节  var offset_GName_FNamePool = 0x30;       // GName 到 FNamePool 的偏移量  var offset_FNamePool_Blocks = 0x10;      // FNamePool 到 Blocks 的偏移量     // FNameEntry相关偏移量和位  var offset_FNameEntry_Info = 0;          // FNameEntry 到 Info 的偏移量  var FNameEntry_LenBit = 6;               // FNameEntry 长度位  var offset_FNameEntry_String = 0x2;      // FNameEntry 到字符串部分的偏移量   // 计算块和偏移量  var Block = index >> 16;                 // 块索引  var Offset = index & 65535;              // 块内偏移量   // 获取FNamePool的起始地址  var FNamePool = GName.add(offset_GName_FNamePool);  // console.log(`FNamePool: ${FNamePool}`);  // console.log(`Block: ${Block}`);     // 获取特定块的地址  var NamePoolChunk = FNamePool.add(offset_FNamePool_Blocks + Block * 8).readPointer();  // console.log(`NamePoolChunk: ${NamePoolChunk}`);     // 计算FNameEntry的地址  var FNameEntry = NamePoolChunk.add(FNameStride * Offset);  // console.log(`FNameEntry: ${FNameEntry}`);     try {      // 读取FNameEntry的Header      if (offset_FNameEntry_Info !== 0) {          var FNameEntryHeader = FNameEntry.add(offset_FNameEntry_Info).readU16();           } else {          var FNameEntryHeader = FNameEntry.readU16();      }  } catch(e) {      // 捕捉读取异常并返回空字符串      // console.log(e);      return "";  }  // console.log(`FNameEntryHeader: ${FNameEntryHeader}`);     // 获取字符串地址  var str_addr = FNameEntry.add(offset_FNameEntry_String);  // console.log(`str_addr: ${str_addr}`);     // 计算字符串长度和宽度  var str_length = FNameEntryHeader >> FNameEntry_LenBit; // 计算字符串长度  var wide = FNameEntryHeader & 1;                       // 判断字符串是否为宽字符     // 如果是宽字符,返回 "widestr"  if (wide) return "widestr";   // 如果字符串长度合理,读取并返回UTF-8字符串  if (str_length > 0 && str_length < 250) {      var str = str_addr.readUtf8String(str_length);      return str;  } else {      return "None"; // 长度不合理,返回 "None"  }}function getActorsAddr(){  var Level_Offset=0x30//偏移  var Actors_Offset=0x98  var Level=GWorld.add(Level_Offset).readPointer()//读取GWorld的level指针  var Actors=Level.add(Actors_Offset).readPointer()//读取Actors的指针  var Actors_Num=Level.add(Actors_Offset).add(8).readU32()//获取Actor的数量  var actorsAddr={};//空对象,下面的实现类似字典  for(var index=0;index 0 && this.getClass(obj) > 0);  // console.log(`isValid: ${isValid}`);  return isValid;}  } function getFNameFromID(index) {  // FNamePool相关偏移量和步长  var FNameStride = 0x2;                   // FNameEntry 的步长,每个FNameEntry占用2字节  var offset_GName_FNamePool = 0x30;       // GName 到 FNamePool 的偏移量  var offset_FNamePool_Blocks = 0x10;      // FNamePool 到 Blocks 的偏移量     // FNameEntry相关偏移量和位  var offset_FNameEntry_Info = 0;          // FNameEntry 到 Info 的偏移量  var FNameEntry_LenBit = 6;               // FNameEntry 长度位  var offset_FNameEntry_String = 0x2;      // FNameEntry 到字符串部分的偏移量   // 计算块和偏移量  var Block = index >> 16;                 // 块索引  var Offset = index & 65535;              // 块内偏移量   // 获取FNamePool的起始地址  var FNamePool = GName.add(offset_GName_FNamePool);  // console.log(`FNamePool: ${FNamePool}`);  // console.log(`Block: ${Block}`);     // 获取特定块的地址  var NamePoolChunk = FNamePool.add(offset_FNamePool_Blocks + Block * 8).readPointer();  // console.log(`NamePoolChunk: ${NamePoolChunk}`);     // 计算FNameEntry的地址  var FNameEntry = NamePoolChunk.add(FNameStride * Offset);  // console.log(`FNameEntry: ${FNameEntry}`);     try {      // 读取FNameEntry的Header      if (offset_FNameEntry_Info !== 0) {          var FNameEntryHeader = FNameEntry.add(offset_FNameEntry_Info).readU16();           } else {          var FNameEntryHeader = FNameEntry.readU16();      }  } catch(e) {      // 捕捉读取异常并返回空字符串      // console.log(e);      return "";  }  // console.log(`FNameEntryHeader: ${FNameEntryHeader}`);     // 获取字符串地址  var str_addr = FNameEntry.add(offset_FNameEntry_String);  // console.log(`str_addr: ${str_addr}`);     // 计算字符串长度和宽度  var str_length = FNameEntryHeader >> FNameEntry_LenBit; // 计算字符串长度  var wide = FNameEntryHeader & 1;                       // 判断字符串是否为宽字符     // 如果是宽字符,返回 "widestr"  if (wide) return "widestr";   // 如果字符串长度合理,读取并返回UTF-8字符串  if (str_length > 0 && str_length < 250) {      var str = str_addr.readUtf8String(str_length);      return str;  } else {      return "None"; // 长度不合理,返回 "None"  }} //获取对象实例function getActorAddr(str){  var player_addr;  var actorsAddr=getActorsAddr();  for(var key in actorsAddr){    if(key==str){      console.log(actorsAddr[key]);      player_addr=actorsAddr[key];    }  }  if(player_addr==null)    {      console.log("null pointer!");    }  return player_addr;}function getActorsAddr(){  var Level_Offset=0x30//偏移  var Actors_Offset=0x98  var Level=GWorld.add(Level_Offset).readPointer()//读取GWorld的level指针  var Actors=Level.add(Actors_Offset).readPointer()//读取Actors的指针  var Actors_Num=Level.add(Actors_Offset).add(8).readU32()//获取Actor的数量  var actorsAddr={};//空对象,下面的实现类似字典  for(var index=0;index3000)//高度大于3000则执行    {                    try{          //setActorHidden(actor_addr)          //setActorCollisionEnabled(actor_addr,1)          SetVisibility(actor_addr,1,0);      }      catch(e){}    }  }}  set("libUE4.so")setPlayerHP();//GetActorVrtualAdress();var b=getActorAddr(playerName);//getActorLocation(b);//getActorLocation(b);dumpActorInstances();//(-1569.0198974609375, -415.2847595214844, 268.3680725097656)//setActorLocation(b,-1500,-400,3000);   //下面这一句代码是指定要Hook的so文件名和要Hook的函数名,函数名就是上面IDA导出表中显示的那个函数var GWorld_Offset=0x0B32D8A8var GName_Offset=0x0B171CC0var GUObjectArray_Offset=0xB1B5F98var playerName="FirstPersonCharacter_C"var moduleBasevar GWorldvar GNamevar GUObjectArray //Class: UObject//对象的内部索引,用于唯一标识对象。 var offset_UObject_InternalIndex = 0xC;  //指向描述对象类的 UClass 对象 var offset_UObject_ClassPrivate = 0x10;   //对象名称在 FName 表中的索引 var offset_UObject_FNameIndex = 0x18;  //指向包含该对象的外部对象,表示层次关系。 var offset_UObject_OuterPrivate = 0x20; var UObject={  getClass: function(obj){    var classPrivate=ptr(obj).add(offset_UObject_ClassPrivate).readPointer();//读取指针    //console.log(`classPrivate: ${classPrivate}`);    return classPrivate;  },getNameId: function(obj){  //console.log(`obj:${obj}`);  try{    var nameId=ptr(obj).add(offset_UObject_FNameIndex).readU32();//读取4字节。    //console.log(`nameId:${nameId}`);    return nameId;  }catch(e){    console.log("error")    return 0;  }},getName: function(obj) {  if (this.isValid(obj)){      return getFNameFromID(this.getNameId(obj));  } else {      return "None";  }},getClassName: function(obj) {  if (this.isValid(obj)) {      var classPrivate = this.getClass(obj);      return this.getName(classPrivate);  } else {      return "None";  }},isValid: function(obj) {  var isValid = (ptr(obj) > 0 && this.getNameId(obj) > 0 && this.getClass(obj) > 0);  // console.log(`isValid: ${isValid}`);  return isValid;}  } function getFNameFromID(index) {  // FNamePool相关偏移量和步长  var FNameStride = 0x2;                   // FNameEntry 的步长,每个FNameEntry占用2字节  var offset_GName_FNamePool = 0x30;       // GName 到 FNamePool 的偏移量  var offset_FNamePool_Blocks = 0x10;      // FNamePool 到 Blocks 的偏移量   登录后可查看完整内容

[培训]内核驱动高级班,冲击BAT一流互联网大厂工作,每周日13:00-18:00直播授课

最后于 2024-6-2 18:16被螺丝兔编辑,原因: #逆向分析

相关推荐: